From b2af4dc2bfe497bdcd04dadbe79350f1740f937f Mon Sep 17 00:00:00 2001 From: robertl Date: Mon, 30 Aug 2004 13:44:01 +0000 Subject: [PATCH] Add IGC for flight recorders. From Chris Jones . --- gpsbabel/Makefile | 3 +- gpsbabel/README | 6 + gpsbabel/README.igc | 93 ++++ gpsbabel/defs.h | 5 + gpsbabel/guibabel | 2 +- gpsbabel/igc.c | 903 ++++++++++++++++++++++++++++++++ gpsbabel/main.c | 1 + gpsbabel/reference/igc1.gpx | 549 +++++++++++++++++++ gpsbabel/reference/igc1_gpx.out | 378 +++++++++++++ gpsbabel/reference/igc1_igc.out | 123 +++++ gpsbabel/reference/igc2.igc | 46 ++ gpsbabel/reference/igc2_gpx.out | 127 +++++ gpsbabel/reference/igc2_igc.out | 32 ++ gpsbabel/route.c | 16 + gpsbabel/testo | 27 + gpsbabel/util.c | 20 + gpsbabel/vecs.c | 7 + 17 files changed, 2336 insertions(+), 2 deletions(-) create mode 100644 gpsbabel/README.igc create mode 100644 gpsbabel/igc.c create mode 100644 gpsbabel/reference/igc1.gpx create mode 100644 gpsbabel/reference/igc1_gpx.out create mode 100644 gpsbabel/reference/igc1_igc.out create mode 100644 gpsbabel/reference/igc2.igc create mode 100644 gpsbabel/reference/igc2_gpx.out create mode 100644 gpsbabel/reference/igc2_igc.out diff --git a/gpsbabel/Makefile b/gpsbabel/Makefile index 33a665e13..e5f3f488c 100644 --- a/gpsbabel/Makefile +++ b/gpsbabel/Makefile @@ -19,7 +19,8 @@ FMTS=magproto.o gpx.o geo.o mapsend.o mapsource.o garmin_tables.o \ psp.o holux.o garmin.o tmpro.o tpg.o \ xcsv.o gcdb.o tiger.o internal_styles.o easygps.o quovadis.o \ gpilots.o saroute.o navicache.o psitrex.o geoniche.o delgpl.o \ - ozi.o nmea.o text.o html.o palmdoc.o netstumbler.o hsa_ndv.o + ozi.o nmea.o text.o html.o palmdoc.o netstumbler.o hsa_ndv.o \ + igc.o FILTERS=position.o duplicate.o arcdist.o polygon.o smplrout.o reverse_route.o sort.o diff --git a/gpsbabel/README b/gpsbabel/README index a89dbc37c..868e40baa 100644 --- a/gpsbabel/README +++ b/gpsbabel/README @@ -593,6 +593,12 @@ THE FORMATS snmac - Always use the MAC address as the shortname. + IGC + + FAI/IGC Data File -- Used by the international gliding community to + record gliding flights. IGC files can be converted to and from tracks + representing recorded flights, and routes representing task + declarations in other formats. DATA FILTERS diff --git a/gpsbabel/README.igc b/gpsbabel/README.igc new file mode 100644 index 000000000..9603c6936 --- /dev/null +++ b/gpsbabel/README.igc @@ -0,0 +1,93 @@ +IGC Data Format Notes. +====================== +Refer to Appendix 1 of http://www.fai.org:81/gliding/gnss/tech_spec_gnss.asp +for the specification of the IGC data format. + +A sample list of software applications that use data in IGC format can be +found at http://www.fai.org:81/gliding/gnss/gnss_analysis_software.pdf + +GPSBabel can be used to translate data in IGC format to and from various other +formats. + +Routes in other formats are used to represent IGC task declarations. +Tracks in other formats are used to represent IGC recorded flights. + + + +Converting to IGC format +======================== +IGC files generated by GPSBabel will NOT pass security validation tests since +the data they contain cannot be proven to originate from an approved flight +recorder. For most software applications that use IGC files this is not an +issue but for competition scoring, record and badge claims the generated files +will not be accepted as proof of a flight. + +A track stored in another format (GPX for example) representing a recorded +flight can be converted into an IGC file: + + gpsbabel -i gpx -f mytrk.gpx -o igc -F myflight.igc + +If multiple track segments are provided in the input file, the one with the +most points will be used. + +A route stored in another format representing a task declaration can be +converted into an IGC file: + + gpsbabel -i gpx -f myrte.gpx -o igc -F mytask.igc + +A route and a track in other formats can be included into a single IGC file: + + gpsbabel -i gpx -f mytrk.gpx -f myrte.gpx -o igc -F myflight.igc + +A similar result can be obtained by downloading the track log and routes +directly from a GPS device connected to a PC. For example to create an IGC +file from data recorded in a Garmin GPS connected to the first serial port of +a PC running Linux: + + gpsbabel -t -r -i garmin -f /dev/ttyS0 -o igc -F myflight.igc + +For Windows operating systems: + + gpsbabel -t -r -i garmin -f com1 -o igc -F myflight.igc + +A waypoint file in another format containing a waypoint whose short name is +"PILOT" can be merged into an IGC file. The description field of the waypoint +will be used for the pilot name in the IGC file header: + + gpsbabel -i gpx -f mytrk.gpx -f myrte.gpx -f mywpt.gpx -o igc -F myflight.igc + gpsbabel -w -t -r -i garmin -f /dev/ttyS0 -o igc -F myflight.igc + +Some formats such as GPX allow routes, tracks and waypoints to exist in the +same file and can be used to fully populate an IGC file: + + gpsbabel -i gpx -f myall.gpx -o igc -F myflight.igc + + + +Converting from IGC format +========================== +Data in an IGC file can be converted into other formats. For example to +generate OziExplorer files containing tracks representing the recorded +flight (myozi.plt) and routes representing declared tasks (myozi.rte): + + gpsbabel -i igc -f myflight.igc -o ozi -F myozi + +Or to GPX format: + + gpsbabel -i igc -f myflight.igc -o gpx -F myflight.gpx + +Header information from the IGC file will be written to the description field +of the track(s). + +If both pressure altitude and GNSS altitude are recorded in the IGC file, two +tracks will be written to the new track file, representing the two altitude +tracks. The latitude, longitude and timestamps in the tracks will be identical. + +A route stored in another format can be merged with an existing IGC file that +has no task declaration, to generate a new IGC file with a task declaration: + + gpsbabel -i igc -f myflight.igc -i gpx -f myrte.gpx -o igc -F mynew.igc + + +Chris Jones +Aug 2004 diff --git a/gpsbabel/defs.h b/gpsbabel/defs.h index a6fbfe7f6..2875ecc5a 100644 --- a/gpsbabel/defs.h +++ b/gpsbabel/defs.h @@ -84,6 +84,7 @@ typedef struct { } global_options; extern global_options global_opts; +extern const char gpsbabel_version[]; /* * Extended data if waypoint happens to represent a geocache. This is @@ -267,8 +268,10 @@ void route_add (waypoint *); void route_add_wpt(route_head *rte, waypoint *wpt); void route_del_wpt(route_head *rte, waypoint *wpt); void route_add_head(route_head *rte); +void route_del_head(route_head *rte); void route_reverse(const route_head *rte_hd); void track_add_head(route_head *rte); +void track_del_head(route_head *rte); void route_disp_all(route_hdr, route_trl, waypt_cb); void track_disp_all(route_hdr, route_trl, waypt_cb); void route_free (route_head *); @@ -431,6 +434,8 @@ void debug_mem_close(); #endif /* DEBUG_MEM */ FILE *xfopen(const char *fname, const char *type, const char *errtxt); +void xfprintf(const char *errtxt, FILE *stream, const char *format, ...); +void xfputs(const char *errtxt, const char *s, FILE *stream); int case_ignore_strcmp(const char *s1, const char *s2); diff --git a/gpsbabel/guibabel b/gpsbabel/guibabel index 3370667c3..7872779a9 100755 --- a/gpsbabel/guibabel +++ b/gpsbabel/guibabel @@ -54,7 +54,7 @@ foreach i {read write} { # TODO: Get this list from 'gpsbabel -?' instead of hardcoding it here. tk_optionMenu $f.ftypes $ftype geo gpsman gpx \ magellan mapsend pcx mapsource gpsutil tiger csv xmap dna psp \ - cetus gpspilot magnav garmin mxf holux ozi tpg + cetus gpspilot magnav garmin mxf holux ozi tpg igc pack $f.lab -side left pack $f.ent -side left -expand yes -fill x pack $f.but -side left diff --git a/gpsbabel/igc.c b/gpsbabel/igc.c new file mode 100644 index 000000000..ef2af2349 --- /dev/null +++ b/gpsbabel/igc.c @@ -0,0 +1,903 @@ +/* + * FAI/IGC data format translation. + * + * Refer to Appendix 1 of + * http://www.fai.org:81/gliding/gnss/tech_spec_gnss.asp for the + * specification of the IGC data format. This translation code was + * written when the latest ammendment list for the specification was AL6. + * + * Copyright (C) 2004 Chris Jones + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 59 Temple Place - Suite 330, Boston, MA 02111 USA + */ + +#include "defs.h" +#include + +static FILE *file_in; +static FILE *file_out; +static char manufacturer[4]; +static const route_head *head; +static char *timeadj = NULL; + +#define MYNAME "IGC" +#define MAXRECLEN 79 // Includes null terminator and CR/LF +#define MAXDESCLEN 1024 +#define PRESTRKNAME "PRESALTTRK" +#define GNSSTRKNAME "GNSSALTTRK" +#define HDRMAGIC "IGCHDRS" +#define HDRDELIM "~" +#define DATEMAGIC "IGCDATE" + +/* + * IGC record types. + * These appear as the first char in each record. + */ +typedef enum { + rec_manuf_id = 'A', // FR manufacturer and identification + rec_fix = 'B', // Fix + rec_task = 'C', // Task/declaration + rec_diff_gps = 'D', // Differential GPS + rec_event = 'E', // Event + rec_constel = 'F', // Constellation + rec_security = 'G', // Security + rec_header = 'H', // File header + rec_fix_defn = 'I', // List of extension data included at end of each fix (B) record + rec_extn_defn = 'J', // List of data included in each extension (K) record + rec_extn_data = 'K', // Extension data + rec_log_book = 'L', // Logbook/comments + + // M..Z are spare + + rec_none = 0, // No record + rec_bad = 1, // Bad record +} igc_rec_type_t; + +/* + * See if two lat/lon pairs are approximately equal. + * @param lat1 The latitude of coordinate pair 1 + * @param lon1 The longitude of coordinate pair 1 + * @param lat2 The latitude of coordinate pair 2 + * @param lon2 The longitude of coordinate pair 2 + * @retval 1 The coordinates are approximately equal + * @retval 0 The coordinates are significantly different + */ +static unsigned char coords_match(double lat1, double lon1, double lat2, double lon2) +{ + return (fabs(lat1 - lat2) < 0.0001 && fabs(lon1 - lon2) < 0.0001) ? 1 : 0; +} + +/************************************************************************************************* + * Input file processing + */ + +/* + * Get an IGC record from the input file + * @param rec Caller allocated storage for the record. At least MAXRECLEN chars must be allocated. + * @return the record type. rec_none on EOF, rec_bad on fgets() or parse error. + */ +static igc_rec_type_t get_record(char *rec) +{ + size_t len; + + if (fgets(rec, MAXRECLEN, file_in) == NULL) { + if (feof(file_in)) { + return rec_none; + } else { + warning(MYNAME " fgets(): %s\n", strerror(errno)); + return rec_bad; + } + } + len = strlen(rec); + if (len < 3 || rec[len - 2] != '\r' || rec[len - 1] != '\n' || rec[0] < 'A' || rec[0] > 'Z') { + warning(MYNAME " bad input record: '%s'\n", rec); + return rec_bad; + } + rec[len - 2] = '\0'; + return (igc_rec_type_t) rec[0]; +} + +static void rd_init(const char *fname) +{ + char ibuf[MAXRECLEN]; + + file_in = xfopen(fname, "rb", MYNAME); + + // File must begin with a manufacturer/ID record + if (get_record(ibuf) != rec_manuf_id || sscanf(ibuf, "A%3[A-Z]", manufacturer) != 1) { + fatal(MYNAME ": %s is not an IGC file\n", fname); + } +} + +static void rd_deinit(void) +{ + fclose(file_in); +} + +/** + * Handle pre- or post-flight task declarations. + * A route is created for each set of waypoints in a task declaration. + * @param rec A single task record + */ +static void igc_task_rec(const char *rec) +{ + static char flight_date[7]; + static unsigned int num_tp, tp_ct; + static route_head *rte_head; + static time_t creation; + + char task_num[5]; + char task_desc[MAXRECLEN]; + waypoint *wpt; + unsigned int lat_deg, lat_min, lat_frac; + unsigned int lon_deg, lon_min, lon_frac; + char lat_hemi[2], lon_hemi[2]; + char short_name[8]; + char tmp_str[MAXRECLEN]; + struct tm tm; + + static enum { id, takeoff, start, turnpoint, finish, landing } state = id; + + // First task record identifies the task to follow + if (id == state) { + task_desc[0] = '\0'; + if (sscanf(rec, "C%2u%2u%2u%2u%2u%2u%6[0-9]%4c%2u%[^\r]\r\n", + &tm.tm_mday, &tm.tm_mon, &tm.tm_year, + &tm.tm_hour, &tm.tm_min, &tm.tm_sec, + flight_date, task_num, &num_tp, task_desc) < 9) { + fatal(MYNAME ": task id (C) record parse error\n'%s'", rec); + } + task_num[4] = '\0'; + tm.tm_mon -= 1; + if (tm.tm_year < 70) { + tm.tm_year += 100; + } + tm.tm_isdst = 0; + creation = mktime(&tm) + get_tz_offset(); + + // Create a route to store the task data in. + rte_head = route_head_alloc(); + rte_head->rte_name = xstrdup(task_num); + sprintf(tmp_str, DATEMAGIC "%s: %s", flight_date, task_desc); + rte_head->rte_desc = xstrdup(tmp_str); + route_add_head(rte_head); + state++; + return; + } + // Get the waypoint + tmp_str[0] = '\0'; + if (sscanf(rec, "C%2u%2u%3u%1[NS]%3u%2u%3u%1[WE]%[^\r]\r\n", + &lat_deg, &lat_min, &lat_frac, lat_hemi, + &lon_deg, &lon_min, &lon_frac, lon_hemi, tmp_str) < 8) { + fatal(MYNAME ": task waypoint (C) record parse error\n%s", rec); + } + + wpt = waypt_new(); + wpt->latitude = ('N' == lat_hemi[0] ? 1 : -1) * + (lat_deg + (lat_min * 1000 + lat_frac) / 1000.0 / 60); + + wpt->longitude = ('E' == lon_hemi[0] ? 1 : -1) * + (lon_deg + (lon_min * 1000 + lon_frac) / 1000.0 / 60); + + wpt->creation_time = creation; + wpt->description = xstrdup(tmp_str); + + // Name the waypoint according to the order of the task record + switch (state) { + case takeoff: + snprintf(short_name, 8, "TAKEOFF"); + state++; + break; + + case start: + snprintf(short_name, 8, "START"); + tp_ct = 0; + state++; + break; + + case turnpoint: + if (++tp_ct == num_tp) { + state++; + } + snprintf(short_name, 8, "TURN%02u", tp_ct); + break; + + case finish: + snprintf(short_name, 8, "FINISH"); + state++; + break; + + case landing: + snprintf(short_name, 8, "LANDING"); + state = id; + break; + + default: + fatal(MYNAME ": task id (C) record internal error\n%s", rec); + break; + } + + // Zero lat and lon indicates an unknown waypoint + if (coords_match(wpt->latitude, wpt->longitude, 0.0, 0.0)) { + waypt_free(wpt); + return; + } + wpt->shortname = xstrdup(short_name); + route_add_wpt(rte_head, wpt); +} + +static void data_read(void) +{ + char ibuf[MAXRECLEN]; + igc_rec_type_t rec_type; + unsigned int hours, mins, secs; + unsigned int lat_deg, lat_min, lat_frac; + unsigned int lon_deg, lon_min, lon_frac; + char lat_hemi[2], lon_hemi[2]; + char validity; + route_head *pres_head = NULL; + route_head *gnss_head = NULL; + int pres_alt, gnss_alt; + char pres_valid = 0; + char gnss_valid = 0; + waypoint *pres_wpt = NULL; + waypoint *gnss_wpt = NULL; + time_t date = 0; + time_t prev_tod = 0; + time_t tod; + struct tm tm; + char tmp_str[20]; + char *hdr_data; + size_t remain; + char trk_desc[MAXDESCLEN + 1]; + + strcpy(trk_desc, HDRMAGIC HDRDELIM); + + while (1) { + rec_type = get_record(ibuf); + switch (rec_type) { + case rec_manuf_id: + // Manufacturer/ID record already found in rd_init(). + warning(MYNAME ": duplicate manufacturer/ID record\n"); + break; + + case rec_header: + // Get the header sub type + if (sscanf(ibuf, "H%*1[FOP]%3s", tmp_str) != 1) { + fatal(MYNAME ": header (H) record parse error\n%s\n%s\n", ibuf, tmp_str); + } + // Optional long name of record sub type is followed by a + // colon. Actual header data follows that. + if (NULL == (hdr_data = strchr(ibuf, ':'))) { + hdr_data = ibuf + 5; + } else { + hdr_data++; + } + + // Date sub type + if (strcmp(tmp_str, "DTE") == 0) { + if (sscanf(hdr_data, "%2u%2u%2u", &tm.tm_mday, &tm.tm_mon, &tm.tm_year) != 3) { + fatal(MYNAME ": date (H) record parse error\n'%s'\n", ibuf); + } + tm.tm_sec = tm.tm_min = tm.tm_hour = 0; + tm.tm_mon -= 1; + if (tm.tm_year < 70) { + tm.tm_year += 100; + } + tm.tm_isdst = 0; + date = mktime(&tm) + get_tz_offset(); + } else { + // Store other header data in the track descriptions + if (strlen(trk_desc) < MAXDESCLEN) { + strcat(ibuf, HDRDELIM); + remain = MAXDESCLEN - strlen(trk_desc); + strncat(trk_desc, ibuf, remain); + } + } + break; + + case rec_fix: + // Date must appear in file before the first fix record + if (date < 1000000L) { + fatal(MYNAME ": bad date %ld\n", date); + } + // Create a track for pressure altitude waypoints + if (!pres_head) { + pres_head = route_head_alloc(); + pres_head->rte_name = xstrdup(PRESTRKNAME); + pres_head->rte_desc = xstrdup(trk_desc); + track_add_head(pres_head); + } + // Create a second track for GNSS altitude waypoints + if (!gnss_head) { + gnss_head = route_head_alloc(); + gnss_head->rte_name = xstrdup(GNSSTRKNAME); + gnss_head->rte_desc = xstrdup(trk_desc); + track_add_head(gnss_head); + } + // Create a waypoint from the fix record data + if (sscanf(ibuf, + "B%2u%2u%2u%2u%2u%3u%1[NS]%3u%2u%3u%1[WE]%c%5d%5d", + &hours, &mins, &secs, &lat_deg, &lat_min, &lat_frac, + lat_hemi, &lon_deg, &lon_min, &lon_frac, lon_hemi, + &validity, &pres_alt, &gnss_alt) != 14) { + fatal(MYNAME ": fix (B) record parse error\n%s\n", ibuf); + } + pres_wpt = waypt_new(); + + pres_wpt->latitude = ('N' == lat_hemi[0] ? 1 : -1) * + (lat_deg + (lat_min * 1000 + lat_frac) / 1000.0 / 60); + + pres_wpt->longitude = ('E' == lon_hemi[0] ? 1 : -1) * + (lon_deg + (lon_min * 1000 + lon_frac) / 1000.0 / 60); + + // Increment date if we pass midnight UTC + tod = (hours * 60 + mins) * 60 + secs; + if (tod < prev_tod) { + date += 24 * 60 * 60; + } + prev_tod = tod; + pres_wpt->creation_time = date + tod; + + // Add the waypoint to the pressure altitude track + if (pres_alt) { + pres_valid = 1; + pres_wpt->altitude = pres_alt; + } else { + pres_wpt->altitude = unknown_alt; + } + route_add_wpt(pres_head, pres_wpt); + + // Add the same waypoint with GNSS altitude to the second + // track + gnss_wpt = waypt_dupe(pres_wpt); + + if (gnss_alt) { + gnss_valid = 1; + gnss_wpt->altitude = gnss_alt; + } else { + gnss_wpt->altitude = unknown_alt; + } + route_add_wpt(gnss_head, gnss_wpt); + break; + + case rec_task: + // Create a route for each pre-flight declaration + igc_task_rec(ibuf); + break; + + case rec_log_book: + // Get the log book sub type + if (sscanf(ibuf, "L%3s", tmp_str) != 1) { + fatal(MYNAME ": log book (L) record parse error\n'%s'\n", ibuf); + } + + if (strcmp(tmp_str, "PFC") == 0) { + // Create a route for each post-flight declaration + igc_task_rec(ibuf + 4); + break; + } else if (global_opts.debug_level) { + if (strcmp(tmp_str, "OOI") == 0) { + fputs(MYNAME ": Observer Input> ", stdout); + } else if (strcmp(tmp_str, "PLT") == 0) { + fputs(MYNAME ": Pilot Input> ", stdout); + } else if (strcmp(tmp_str, manufacturer) == 0) { + fputs(MYNAME ": Manufacturer Input> ", stdout); + } else { + fputs(MYNAME ": Anonymous Input> ", stdout); + fputs(ibuf + 1, stdout); + break; + } + fputs(ibuf + 4, stdout); + putchar('\n'); + } + break; + + // These record types are discarded + case rec_diff_gps: + case rec_event: + case rec_constel: + case rec_security: + case rec_fix_defn: + case rec_extn_defn: + case rec_extn_data: + break; + + // No more records + case rec_none: + + // Include pressure altitude track only if it has useful + // altitude data or if it is the only track available. + if (pres_head && !pres_valid && gnss_head) { + track_del_head(pres_head); + pres_head = NULL; + } + // Include GNSS altitude track only if it has useful altitude + // data or if it is the only track available. + if (gnss_head && !gnss_valid && pres_head) { + track_del_head(gnss_head); + } + return; // All done so bail + + default: + case rec_bad: + fatal(MYNAME ": failure reading file\n"); + break; + } + } +} + +/************************************************************************************************* + * Output file processing + */ + +/************************************************* + * Callbacks used to scan for specific track types + */ + +static void detect_pres_track(const route_head * rh) +{ + if (rh->rte_name && strncmp(rh->rte_name, PRESTRKNAME, 6) == 0) { + head = rh; + } +} + +static void detect_gnss_track(const route_head * rh) +{ + if (rh->rte_name && strncmp(rh->rte_name, GNSSTRKNAME, 6) == 0) { + head = rh; + } +} + +static void detect_other_track(const route_head * rh) +{ + static int max_waypt_ct; + + if (!head) { + max_waypt_ct = 0; + } + // Find other track with the most waypoints + if (rh->rte_waypt_ct > max_waypt_ct && + (!rh->rte_name || + (strncmp(rh->rte_name, PRESTRKNAME, 6) != 0 && + strncmp(rh->rte_name, GNSSTRKNAME, 6) != 0))) { + head = rh; + max_waypt_ct = rh->rte_waypt_ct; + } +} + +/* + * Identify the pressure altitude and GNSS altitude tracks. + * @param pres_track Set by the function to the pressure altitude track + * head. NULL if not found. + * @param gnss_track Set by the function to the GNSS altitude track + * head. NULL if not found. + */ +static void get_tracks(const route_head ** pres_track, const route_head ** gnss_track) +{ + head = NULL; + track_disp_all(detect_pres_track, NULL, NULL); + *pres_track = head; + + head = NULL; + track_disp_all(detect_gnss_track, NULL, NULL); + *gnss_track = head; + + head = NULL; + track_disp_all(detect_other_track, NULL, NULL); + + if (!*pres_track && *gnss_track && head) { + *pres_track = head; + } + + if (!*gnss_track && head) { + *gnss_track = head; + } +} + +/************************************************* + * IGC string formatting functions + */ + +static char *latlon2str(const waypoint * wpt) +{ + static char str[18] = ""; + char lat_hemi = wpt->latitude < 0 ? 'S' : 'N'; + char lon_hemi = wpt->longitude < 0 ? 'W' : 'E'; + unsigned char lat_deg = fabs(wpt->latitude); + unsigned char lon_deg = fabs(wpt->longitude); + unsigned int lat_min = (fabs(wpt->latitude) - lat_deg) * 60000 + 0.5; + unsigned int lon_min = (fabs(wpt->longitude) - lon_deg) * 60000 + 0.5; + + if (snprintf(str, 18, "%02u%05u%c%03u%05u%c", + lat_deg, lat_min, lat_hemi, lon_deg, lon_min, lon_hemi) != 17) { + fatal(MYNAME ": Bad waypoint format '%s'\n", str); + } + return str; +} + +static char *date2str(struct tm *dt) +{ + static char str[7] = ""; + + if (snprintf(str, 7, "%02u%02u%02u", dt->tm_mday, dt->tm_mon + 1, dt->tm_year % 100) != 6) { + fatal(MYNAME ": Bad date format '%s'\n", str); + } + return str; +} + +static char *tod2str(struct tm *tod) +{ + static char str[7] = ""; + + if (snprintf(str, 7, "%02u%02u%02u", tod->tm_hour, tod->tm_min, tod->tm_sec) != 6) { + fatal(MYNAME ": Bad time of day format '%s'\n", str); + } + return str; +} + +/* + * Write header records + */ +static void wr_header(void) +{ + const route_head *pres_track; + const route_head *track; + struct tm *tm; + time_t date; + static const char dflt_str[] = "Unknown"; + const char *str; + waypoint *wpt; + + get_tracks(&pres_track, &track); + if (!track && pres_track) { + track = pres_track; + } + // Date in header record is that of the first fix record + date = !track ? current_time() : + ((waypoint *) QUEUE_FIRST(&track->waypoint_list))->creation_time; + + if (NULL == (tm = gmtime(&date))) { + fatal(MYNAME ": Bad track timestamp\n"); + } + xfprintf(MYNAME, file_out, "HFDTE%s\r\n", date2str(tm)); + + // Other header data may have been stored in track description + if (track && track->rte_desc && strncmp(track->rte_desc, HDRMAGIC, strlen(HDRMAGIC)) == 0) { + for (str = strtok(track->rte_desc + strlen(HDRMAGIC) + strlen(HDRDELIM), HDRDELIM); + str; str = strtok(NULL, HDRDELIM)) { + xfprintf(MYNAME, file_out, "%s\r\n", str); + } + } else { + // IGC header info not found so synthesise it. + // If a waypoint is supplied with a short name of "PILOT", use + // its description as the pilot's name in the header. + str = dflt_str; + if (NULL != (wpt = find_waypt_by_name("PILOT")) && wpt->description) { + str = wpt->description; + } + xfprintf(MYNAME, file_out, "HFPLTPILOT:%s\r\n", str); + } +} + +/************************************************* + * Generation of IGC task declaration records + */ + +static void wr_task_wpt_name(const waypoint * wpt, const char *alt_name) +{ + xfprintf(MYNAME, file_out, "C%s%s\r\n", latlon2str(wpt), + wpt->description ? wpt->description : wpt->shortname ? wpt->shortname : alt_name); +} + +static void wr_task_hdr(const route_head * rte) +{ + unsigned char have_takeoff = 0; + const waypoint *wpt; + char flight_date[7] = "000000"; + char task_desc[MAXRECLEN] = ""; + int num_tps = rte->rte_waypt_ct - 2; + struct tm *tm; + time_t rte_time; + static unsigned int task_num = 1; + + if (num_tps < 0) { + fatal(MYNAME ": Empty task route\n"); + } + // See if the takeoff and landing waypoints are there or if we need to + // generate them. + wpt = (waypoint *) QUEUE_LAST(&rte->waypoint_list); + if (wpt->shortname && strncmp(wpt->shortname, "LANDING", 6) == 0) { + num_tps--; + } + wpt = (waypoint *) QUEUE_FIRST(&rte->waypoint_list); + if (wpt->shortname && strncmp(wpt->shortname, "TAKEOFF", 6) == 0) { + have_takeoff = 1; + num_tps--; + } + if (num_tps < 0) { + fatal(MYNAME ": Too few waypoints in task route\n"); + } + // Gather data to write to the task identification (first) record + rte_time = wpt->creation_time ? wpt->creation_time : current_time(); + if (NULL == (tm = gmtime(&rte_time))) { + fatal(MYNAME ": Bad task route timestamp\n"); + } + + if (rte->rte_desc) { + sscanf(rte->rte_desc, DATEMAGIC "%6[0-9]: %s", flight_date, task_desc); + } + + xfprintf(MYNAME, file_out, "C%s%s%s%04u%02u%s\r\n", date2str(tm), + tod2str(tm), flight_date, task_num++, num_tps, task_desc); + + if (!have_takeoff) { + // Generate the takeoff waypoint + wr_task_wpt_name(wpt, "TAKEOFF"); + } +} + +static void wr_task_wpt(const waypoint * wpt) +{ + wr_task_wpt_name(wpt, ""); +} + +static void wr_task_tlr(const route_head * rte) +{ + // If the landing waypoint is not supplied we need to generate it. + const waypoint *wpt = (waypoint *) QUEUE_LAST(&rte->waypoint_list); + if (!wpt->shortname || strncmp(wpt->shortname, "LANDIN", 6) != 0) { + wr_task_wpt_name(wpt, "LANDING"); + } +} + +static void wr_tasks(void) +{ + route_disp_all(wr_task_hdr, wr_task_tlr, wr_task_wpt); +} + +/* + * Write a single fix record + */ +static void wr_fix_record(const waypoint * wpt, int pres_alt, int gnss_alt) +{ + struct tm *tm; + + if (NULL == (tm = gmtime(&wpt->creation_time))) { + fatal(MYNAME ": bad track timestamp\n"); + } + + if (unknown_alt == pres_alt) { + pres_alt = 0; + } + if (unknown_alt == gnss_alt) { + gnss_alt = 0; + } + xfprintf(MYNAME, file_out, "B%02u%02u%02u%sA%05d%05d\r\n", tm->tm_hour, + tm->tm_min, tm->tm_sec, latlon2str(wpt), pres_alt, gnss_alt); +} + +/** + * Attempt to align the pressure and GNSS tracks in time. + * This is useful when trying to merge a track (lat/lon/time) recorded by a + * GPS with a barograph (alt/time) recorded by a seperate instrument with + * independent clocks which are not closely synchronised. + * @return The number of seconds to add to the GNSS track in order to align + * it with the pressure track. + */ +static int correlate_tracks(const route_head * pres_track, const route_head * gnss_track) +{ + const queue *elem; + double last_alt, alt_diff; + double speed; + time_t pres_time, gnss_time; + int time_diff; + const waypoint *wpt; + + // Deduce the landing time from the pressure altitude track based on + // when we last descended to within 10m of the final track altitude. + elem = QUEUE_LAST(&pres_track->waypoint_list); + last_alt = ((waypoint *) elem)->altitude; + do { + elem = elem->prev; + if (&pres_track->waypoint_list == elem) { + // No track left + return 0; + } + alt_diff = last_alt - ((waypoint *) elem)->altitude; + if (alt_diff > 10.0) { + // Last part of track was ascending + return 0; + } + } while (alt_diff > -10.0); + pres_time = ((waypoint *) elem->next)->creation_time; + if (global_opts.debug_level >= 1) { + printf(MYNAME ": pressure landing time %s", ctime(&pres_time)); + } + // Deduce the landing time from the GNSS altitude track based on + // when the groundspeed last dropped below a certain level. + elem = QUEUE_LAST(&gnss_track->waypoint_list); + last_alt = ((waypoint *) elem)->altitude; + do { + wpt = (waypoint *) elem; + elem = elem->prev; + if (&gnss_track->waypoint_list == elem) { + // No track left + return 0; + } + // Get a crude indication of groundspeed from the change in lat/lon + time_diff = wpt->creation_time - ((waypoint *) elem)->creation_time; + speed = !time_diff ? 0 : + (fabs(wpt->latitude - ((waypoint *) elem)->latitude) + + fabs(wpt->longitude - ((waypoint *) elem)->longitude)) / time_diff; + if (global_opts.debug_level >= 2) { + printf(MYNAME ": speed=%f\n", speed); + } + } while (speed < 0.00003); + gnss_time = ((waypoint *) elem->next)->creation_time; + if (global_opts.debug_level >= 1) { + printf(MYNAME ": gnss landing time %s", ctime(&gnss_time)); + } + // Time adjustment is difference between the two estimated landing times + if (15 * 60 < abs(time_diff = pres_time - gnss_time)) { + warning(MYNAME ": excessive time adjustment %ds\n", time_diff); + } + return time_diff; +} + +/** + * Interpolate altitude from a track at a given time. + * @param track The track containing altitude data. + * @param time The time that we are interested in. + * @return The altitude interpolated from the track. + */ +static double interpolate_alt(const route_head * track, time_t time) +{ + static const queue *prev_elem = NULL; + static const queue *curr_elem = NULL; + const waypoint *prev_wpt; + const waypoint *curr_wpt; + int time_diff; + double alt_diff; + + // Start search at the beginning of the track + if (!prev_elem) { + curr_elem = prev_elem = QUEUE_FIRST(&track->waypoint_list); + } + // Find the track points either side of the requested time + while (((waypoint *) curr_elem)->creation_time < time) { + if (QUEUE_LAST(&track->waypoint_list) == curr_elem) { + // Requested time later than all track points, we can't interpolate + return unknown_alt; + } + prev_elem = curr_elem; + curr_elem = QUEUE_NEXT(prev_elem); + } + + prev_wpt = (waypoint *) prev_elem; + curr_wpt = (waypoint *) curr_elem; + + if (QUEUE_FIRST(&track->waypoint_list) == curr_elem) { + if (curr_wpt->creation_time == time) { + // First point's creation time is an exact match so use it's altitude + return curr_wpt->altitude; + } else { + // Requested time is prior to any track points, we can't interpolate + return unknown_alt; + } + } + // Interpolate + if (0 == (time_diff = curr_wpt->creation_time - prev_wpt->creation_time)) { + // Avoid divide by zero + return curr_wpt->altitude; + } + alt_diff = curr_wpt->altitude - prev_wpt->altitude; + return prev_wpt->altitude + (alt_diff / time_diff) * (time - prev_wpt->creation_time); +} + +/* + * Pressure altitude and GNSS altitude may be provided in two seperate + * tracks. This function attempts to merge them into one. + */ +static void wr_track(void) +{ + const route_head *pres_track; + const route_head *gnss_track; + const waypoint *wpt; + const queue *elem; + const queue *tmp; + int time_adj; + double pres_alt; + + // Find pressure altitude and GNSS altitude tracks + get_tracks(&pres_track, &gnss_track); + + // If both found, attempt to merge them + if (pres_track && gnss_track) { + if (timeadj) { + if (strcmp(timeadj, "auto") == 0) { + time_adj = correlate_tracks(pres_track, gnss_track); + } else if (sscanf(timeadj, "%d", &time_adj) != 1) { + fatal(MYNAME ": bad timeadj argument '%s'\n", timeadj); + } + } else { + time_adj = 0; + } + if (global_opts.debug_level >= 1) { + printf(MYNAME ": adjusting time by %ds\n", time_adj); + } + // Iterate through waypoints in both tracks simultaneously + QUEUE_FOR_EACH(&gnss_track->waypoint_list, elem, tmp) { + wpt = (waypoint *) elem; + pres_alt = interpolate_alt(pres_track, wpt->creation_time + time_adj); + wr_fix_record(wpt, pres_alt, wpt->altitude); + } + } else { + if (pres_track) { + // Only the pressure altitude track was found so generate fix + // records from it alone. + QUEUE_FOR_EACH(&pres_track->waypoint_list, elem, tmp) { + wr_fix_record((waypoint *) elem, ((waypoint *) elem)->altitude, unknown_alt); + } + } else if (gnss_track) { + // Only the GNSS altitude track was found so generate fix + // records from it alone. + QUEUE_FOR_EACH(&gnss_track->waypoint_list, elem, tmp) { + wr_fix_record((waypoint *) elem, unknown_alt, ((waypoint *) elem)->altitude); + } + } else { + // No tracks found so nothing to do + return; + } + } +} + +static void wr_init(const char *fname) +{ + file_out = xfopen(fname, "wb", MYNAME); +} + +static void wr_deinit(void) +{ + fclose(file_out); +} + +static void data_write(void) +{ + xfputs(MYNAME, "AXXXZZZGPSBabel\r\n", file_out); + wr_header(); + wr_tasks(); + wr_track(); + xfprintf(MYNAME, file_out, "LXXXGenerated by GPSBabel Version %s\r\n", gpsbabel_version); + xfputs(MYNAME, "GGPSBabelSecurityRecordGuaranteedToFailVALIChecks\r\n", file_out); +} + + +static arglist_t igc_args[] = { + {"timeadj", &timeadj, + "(integer sec or 'auto') Barograph to GPS time diff", ARGTYPE_STRING}, + {0, 0, 0, 0} +}; + +ff_vecs_t igc_vecs = { + ff_type_file, + rd_init, + wr_init, + rd_deinit, + wr_deinit, + data_read, + data_write, + igc_args +}; diff --git a/gpsbabel/main.c b/gpsbabel/main.c index a93026afc..576bd673c 100644 --- a/gpsbabel/main.c +++ b/gpsbabel/main.c @@ -22,6 +22,7 @@ #include global_options global_opts; +const char gpsbabel_version[] = VERSION; static void usage(const char *pname, int shorter) diff --git a/gpsbabel/reference/igc1.gpx b/gpsbabel/reference/igc1.gpx new file mode 100644 index 000000000..35191c176 --- /dev/null +++ b/gpsbabel/reference/igc1.gpx @@ -0,0 +1,549 @@ + + + + + + PALLMG + PALLMG + PALLMG + dot + + + PILOT + CHRIS JONES + CHRIS JONES + dot + + + PLUMTH + PLUMTHORPE LOC + PLUMTHORPE LOC + dot + + + + +0.000000 + + + +0.000000 + + + + + + #2 +1 + + +0.000000 + + + +0.000000 + + + + + + #3 +2 + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + + + + #4 +3 + + +0.000000 + + + +0.000000 + + + +0.000000 + + + +0.000000 + + + + + + AB1 + + 0.000000 + BORAH + + + 0.000000 + BALDWI + + + 0.000000 + MANAIR + + + diff --git a/gpsbabel/reference/igc1_gpx.out b/gpsbabel/reference/igc1_gpx.out new file mode 100644 index 000000000..bfa51235f --- /dev/null +++ b/gpsbabel/reference/igc1_gpx.out @@ -0,0 +1,378 @@ + + + + + GNSSALTTRK + IGCHDRS~HFPLTPILOT:CHRIS JONES~ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0001 + IGCDATE000000: + + TAKEOFF + BORAH + + + START + BORAH + + + TURN01 + BALDWI + + + FINISH + MANAIR + + + LANDING + MANAIR + + + diff --git a/gpsbabel/reference/igc1_igc.out b/gpsbabel/reference/igc1_igc.out new file mode 100644 index 000000000..beb86d8e2 --- /dev/null +++ b/gpsbabel/reference/igc1_igc.out @@ -0,0 +1,123 @@ +AXXXZZZGPSBabel +HFDTE240404 +HFPLTPILOT:CHRIS JONES +C010170000000000000000101 +C3040552S15036542EBORAH +C3040552S15036542EBORAH +C3043796S15038952EBALDWI +C3045821S15043392EMANAIR +C3045821S15043392EMANAIR +B0400193040579S15036384EA0000000000 +B0400493040614S15036317EA0000000000 +B0401193040699S15036326EA0000000000 +B0401503040629S15036431EA0000000000 +B0402203040628S15036565EA0000000000 +B0402513040689S15036431EA0000000000 +B0403223040555S15036270EA0000000000 +B0403523040720S15036356EA0000000000 +B0404233040884S15036452EA0000000000 +B0404543040658S15036419EA0000000000 +B0405243040811S15036507EA0000000000 +B0405553040688S15036378EA0000000000 +B0406263040714S15036409EA0000000000 +B0406573040686S15036391EA0000000000 +B0407283040728S15036419EA0000000000 +B0407583040656S15036366EA0000000000 +B0408293040785S15036447EA0000000000 +B0409003040822S15036433EA0000000000 +B0409313040795S15036535EA0000000000 +B0410013040809S15036573EA0000000000 +B0410323040819S15036444EA0000000000 +B0411023040785S15036304EA0000000000 +B0411323040816S15036327EA0000000000 +B0412033040846S15036397EA0000000000 +B0412343040813S15036503EA0000000000 +B0413043040760S15036508EA0000000000 +B0413353040694S15036319EA0000000000 +B0414063040538S15036152EA0000000000 +B0414373040695S15036308EA0000000000 +B0415073040758S15036421EA0000000000 +B0415383040766S15036490EA0000000000 +B0416083040753S15036493EA0000000000 +B0416393040739S15036494EA0000000000 +B0417103040750S15036527EA0000000000 +B0417413040819S15036393EA0000000000 +B0418113040806S15036352EA0000000000 +B0418413040569S15036344EA0000000000 +B0419123040526S15036253EA0000000000 +B0419433040646S15036334EA0000000000 +B0420133040794S15036367EA0000000000 +B0420443040707S15036367EA0000000000 +B0421143040678S15036423EA0000000000 +B0421443040658S15036414EA0000000000 +B0422153040772S15036358EA0000000000 +B0422453040722S15036423EA0000000000 +B0423153040717S15036457EA0000000000 +B0423453040700S15036444EA0000000000 +B0424163040689S15036457EA0000000000 +B0424473040753S15036486EA0000000000 +B0425183040750S15036329EA0000000000 +B0425483040794S15036240EA0000000000 +B0426193040688S15036353EA0000000000 +B0426493040679S15036400EA0000000000 +B0427203040754S15036332EA0000000000 +B0427513040693S15036316EA0000000000 +B0428213040918S15036288EA0000000000 +B0428513041173S15036316EA0000000000 +B0429223041467S15036406EA0000000000 +B0429523041752S15036547EA0000000000 +B0430233042022S15036681EA0000000000 +B0430543042288S15036906EA0000000000 +B0431243042359S15037048EA0000000000 +B0431553042430S15037151EA0000000000 +B0432263042348S15037228EA0000000000 +B0432573042410S15037256EA0000000000 +B0433273042612S15037247EA0000000000 +B0433583042857S15037346EA0000000000 +B0434293043064S15037635EA0000000000 +B0435003043168S15037848EA0000000000 +B0435303043193S15037985EA0000000000 +B0436013043182S15037825EA0000000000 +B0436313043184S15037915EA0000000000 +B0437023043122S15038026EA0000000000 +B0437333043136S15037990EA0000000000 +B0438033043131S15038063EA0000000000 +B0438333043209S15037994EA0000000000 +B0439043043449S15038115EA0000000000 +B0439353043669S15038384EA0000000000 +B0440053043786S15038748EA0000000000 +B0440353043743S15038849EA0000000000 +B0441053043773S15038844EA0000000000 +B0441353043804S15038931EA0000000000 +B0442053043778S15039051EA0000000000 +B0442363043815S15038859EA0000000000 +B0443073043844S15038931EA0000000000 +B0443383043886S15038927EA0000000000 +B0444093043845S15038947EA0000000000 +B0444403043904S15038862EA0000000000 +B0445113043934S15038756EA0000000000 +B0445423044092S15038774EA0000000000 +B0446133044150S15039144EA0000000000 +B0446443044370S15039248EA0000000000 +B0447153044594S15039415EA0000000000 +B0447463044804S15039714EA0000000000 +B0448163044963S15040024EA0000000000 +B0448473044982S15040431EA0000000000 +B0449183044991S15040573EA0000000000 +B0449483045154S15040808EA0000000000 +B0450193045342S15041095EA0000000000 +B0450493045484S15041383EA0000000000 +B0451193045595S15041771EA0000000000 +B0451503045744S15042205EA0000000000 +B0452213045844S15042642EA0000000000 +B0452513045936S15042952EA0000000000 +B0453213045839S15043315EA0000000000 +B0453523045915S15043559EA0000000000 +B0454233045918S15043429EA0000000000 +B0454533045933S15043539EA0000000000 +B0455243045817S15043457EA0000000000 +B0455543045823S15043438EA0000000000 +B0456253045831S15043422EA0000000000 +B0456563045845S15043414EA0000000000 +B0457193045848S15043404EA0000000000 +GGPSBabelSecurityRecordGuaranteedToFailVALIChecks diff --git a/gpsbabel/reference/igc2.igc b/gpsbabel/reference/igc2.igc new file mode 100644 index 000000000..b07594187 --- /dev/null +++ b/gpsbabel/reference/igc2.igc @@ -0,0 +1,46 @@ +AXXXABCFLIGHT:1 +HFFXA035 +HFDTE160701 +HFPLTPILOT:Bill Bloggs +HFGTYGLIDERTYPE:Schleicher ASH-25 +HFGIDGLIDERID:ABCD-1234 +HFDTM100GPSDATUM:WGS-1984 +HFRFWFIRMWAREVERSION:6.4 +HFRHWHARDWAREVERSION:3.0 +HFFTYFRTYPE:Manufacturer Model +HFGPSMarconiCanada:Superstar 12ch max10000m +HFPRSPRESSALTSENSOR:Sensyn XYZ1111 max11000m +HFCIDCOMPETITIONID:XYZ-78910 +HFCCLCOMPETITIONCLASS:15mMotor Glider +HFSCMSECONDCREW:JohnSmith +I023638FXA3941ENL +J010812HDT +C150701213841160701000102500KTri +C5111359N00101899WLashamClubhouse +C5110179N00102644WLashamStart S Start +C5209092N00255227WSarnesfield TP1 +C5230147N00017612WNormanCross TP2 +C5110179N00102644WLashamStart S Finish +C5111359N00101899WLashamClubhouse +F160240040609123624221821 +B1602405407121N00249342WA0028000421020509950 +D20331 +E160245PEV +B1602455107126N00149300WA0028800429019509020 +B1602505107134N00149283WA0029000432021009015 +B1602555107140N00149221WA0029000430020009012 +F1603000609123624221821 +B1603005107150N00149202WA0029100432025608009 +E160305PEV +B1603055107180N00149185WA0029100435021008015 +B1603105107212N00149174WA0029300435019608024 +K160248090 +B1602485107220N00149150WA0049400436019008018 +B1602525107330N00149127WA0049600439019508015 +LXXXRURITANIANSTANDARD NATIONALS DAY 1 +LXXXFLIGHTTIME: 4:14:25 TASK SPEED:58.48KTS +GREJNGJERJKNJKRE31895478537H43982FJN9248F942389T433T +GJNJK1489IERGNV3089IVJE9GO398535J3894N358954983O0934 +GSKTO5427FGTNUT5621WKTC6714FT8957FGMKJ134527FGTR6751 +GK2489IERGNV3089IVJE39GO398535J3894N358954983FTGY546 +G12560DJUWT28719GTAOL5628FGWNIST78154INWTOLP7815FITN diff --git a/gpsbabel/reference/igc2_gpx.out b/gpsbabel/reference/igc2_gpx.out new file mode 100644 index 000000000..8655b694f --- /dev/null +++ b/gpsbabel/reference/igc2_gpx.out @@ -0,0 +1,127 @@ + + + + + PRESALTTRK + IGCHDRS~HFFXA035~HFPLTPILOT:Bill Bloggs~HFGTYGLIDERTYPE:Schleicher ASH-25~HFGIDGLIDERID:ABCD-1234~HFDTM100GPSDATUM:WGS-1984~HFRFWFIRMWAREVERSION:6.4~HFRHWHARDWAREVERSION:3.0~HFFTYFRTYPE:Manufacturer Model~HFGPSMarconiCanada:Superstar 12ch max10000m~HFPRSPRESSALTSENSOR:Sensyn XYZ1111 max11000m~HFCIDCOMPETITIONID:XYZ-78910~HFCCLCOMPETITIONCLASS:15mMotor Glider~HFSCMSECONDCREW:JohnSmith~ + + +280.000000 + + + +288.000000 + + + +290.000000 + + + +290.000000 + + + +291.000000 + + + +291.000000 + + + +293.000000 + + + +494.000000 + + + +496.000000 + + + + + + GNSSALTTRK + IGCHDRS~HFFXA035~HFPLTPILOT:Bill Bloggs~HFGTYGLIDERTYPE:Schleicher ASH-25~HFGIDGLIDERID:ABCD-1234~HFDTM100GPSDATUM:WGS-1984~HFRFWFIRMWAREVERSION:6.4~HFRHWHARDWAREVERSION:3.0~HFFTYFRTYPE:Manufacturer Model~HFGPSMarconiCanada:Superstar 12ch max10000m~HFPRSPRESSALTSENSOR:Sensyn XYZ1111 max11000m~HFCIDCOMPETITIONID:XYZ-78910~HFCCLCOMPETITIONCLASS:15mMotor Glider~HFSCMSECONDCREW:JohnSmith~ + + +421.000000 + + + +429.000000 + + + +432.000000 + + + +430.000000 + + + +432.000000 + + + +435.000000 + + + +435.000000 + + + +436.000000 + + + +439.000000 + + + + + + 0001 + IGCDATE160701: 500KTri + + + TAKEOFF + LashamClubhouse + + + + START + LashamStart S Start + + + + TURN01 + Sarnesfield TP1 + + + + TURN02 + NormanCross TP2 + + + + FINISH + LashamStart S Finish + + + + LANDING + LashamClubhouse + + + diff --git a/gpsbabel/reference/igc2_igc.out b/gpsbabel/reference/igc2_igc.out new file mode 100644 index 000000000..877a5bcba --- /dev/null +++ b/gpsbabel/reference/igc2_igc.out @@ -0,0 +1,32 @@ +AXXXZZZGPSBabel +HFDTE160701 +HFFXA035 +HFPLTPILOT:Bill Bloggs +HFGTYGLIDERTYPE:Schleicher ASH-25 +HFGIDGLIDERID:ABCD-1234 +HFDTM100GPSDATUM:WGS-1984 +HFRFWFIRMWAREVERSION:6.4 +HFRHWHARDWAREVERSION:3.0 +HFFTYFRTYPE:Manufacturer Model +HFGPSMarconiCanada:Superstar 12ch max10000m +HFPRSPRESSALTSENSOR:Sensyn XYZ1111 max11000m +HFCIDCOMPETITIONID:XYZ-78910 +HFCCLCOMPETITIONCLASS:15mMotor Glider +HFSCMSECONDCREW:JohnSmith +C150701213841160701000102500KTri +C5111359N00101899WLashamClubhouse +C5110179N00102644WLashamStart S Start +C5209092N00255227WSarnesfield TP1 +C5230147N00017612WNormanCross TP2 +C5110179N00102644WLashamStart S Finish +C5111359N00101899WLashamClubhouse +B1602405407121N00249342WA0028000421 +B1602455107126N00149300WA0028800429 +B1602505107134N00149283WA0029000432 +B1602555107140N00149221WA0029000430 +B1603005107150N00149202WA0029100432 +B1603055107180N00149185WA0029100435 +B1603105107212N00149174WA0029300435 +B1602485107220N00149150WA0049400436 +B1602525107330N00149127WA0049600439 +GGPSBabelSecurityRecordGuaranteedToFailVALIChecks diff --git a/gpsbabel/route.c b/gpsbabel/route.c index 70c6dd4ea..0f0733087 100644 --- a/gpsbabel/route.c +++ b/gpsbabel/route.c @@ -70,6 +70,14 @@ route_add_head(route_head *rte) rte_head_ct++; } +void +route_del_head(route_head *rte) +{ + dequeue( &rte->Q ); + route_free( rte ); + rte_head_ct--; +} + void track_add_head(route_head *rte) { @@ -78,6 +86,14 @@ track_add_head(route_head *rte) trk_head_ct++; } +void +track_del_head(route_head *rte) +{ + dequeue( &rte->Q ); + route_free( rte ); + trk_head_ct--; +} + void route_add_wpt(route_head *rte, waypoint *wpt) { diff --git a/gpsbabel/testo b/gpsbabel/testo index 07a0eff1c..677bb9bf0 100755 --- a/gpsbabel/testo +++ b/gpsbabel/testo @@ -522,6 +522,33 @@ rm -f ${TMPDIR}/netstumbler.mps ${PNAME} -i netstumbler -f reference/netstumbler.txt -o mapsource -F ${TMPDIR}/netstumbler.mps bincompare ${TMPDIR}/netstumbler.mps reference/netstumbler.mps +# +# IGC tests +# +rm -f ${TMPDIR}/igc*out +${PNAME} -i gpx -f reference/igc1.gpx -o igc -F ${TMPDIR}/igc.out +sed '/^LXXXGenerated by GPSBabel Version/d' ${TMPDIR}/igc.out > ${TMPDIR}/igc_sed.out +compare ${TMPDIR}/igc_sed.out reference/igc1_igc.out + +${PNAME} -i igc -f ${TMPDIR}/igc.out -o gpx -F ${TMPDIR}/igc.gpx +compare ${TMPDIR}/igc.gpx reference/igc1_gpx.out + +${PNAME} -i gpx -f ${TMPDIR}/igc.gpx -o igc -F ${TMPDIR}/igc.out +sed '/^LXXXGenerated by GPSBabel Version/d' ${TMPDIR}/igc.out > ${TMPDIR}/igc_sed.out +compare ${TMPDIR}/igc_sed.out reference/igc1_igc.out + + +${PNAME} -i igc -f reference/igc2.igc -o gpx -F ${TMPDIR}/igc.gpx +compare ${TMPDIR}/igc.gpx reference/igc2_gpx.out + +${PNAME} -i gpx -f ${TMPDIR}/igc.gpx -o igc -F ${TMPDIR}/igc.out +sed '/^LXXXGenerated by GPSBabel Version/d' ${TMPDIR}/igc.out > ${TMPDIR}/igc_sed.out +compare ${TMPDIR}/igc_sed.out reference/igc2_igc.out + +${PNAME} -i igc -f ${TMPDIR}/igc.out -o gpx -F ${TMPDIR}/igc.gpx +compare ${TMPDIR}/igc.gpx reference/igc2_gpx.out + + # # XCSV "human readable" tests # diff --git a/gpsbabel/util.c b/gpsbabel/util.c index dba2832e3..32d54baeb 100644 --- a/gpsbabel/util.c +++ b/gpsbabel/util.c @@ -255,6 +255,26 @@ xfopen(const char *fname, const char *type, const char *errtxt) return f; } +void +xfprintf(const char *errtxt, FILE *stream, const char *format, ...) +{ + va_list ap; + va_start(ap, format); + if (vfprintf(stream, format, ap) < 0) { + fatal("%s writing output file. Error was '%s'.\n", + errtxt, strerror(errno)); + } + va_end(ap); +} + +void +xfputs(const char *errtxt, const char *s, FILE *stream) +{ + if (fputs(s, stream) < 0) { + fatal("%s Writing output file. Error was '%s'.\n", + errtxt, strerror(errno)); + } +} /* * Duplicate a pascal string into a normal C string. diff --git a/gpsbabel/vecs.c b/gpsbabel/vecs.c index 4cbfa8973..c82a96c3d 100644 --- a/gpsbabel/vecs.c +++ b/gpsbabel/vecs.c @@ -65,6 +65,7 @@ extern ff_vecs_t palmdoc_vecs; extern ff_vecs_t html_vecs; extern ff_vecs_t netstumbler_vecs; extern ff_vecs_t HsaEndeavourNavigator_vecs; +extern ff_vecs_t igc_vecs; static vecs_t vec_list[] = { @@ -279,6 +280,12 @@ vecs_t vec_list[] = { "HSA Endeavour Navigator export File", NULL }, + { + &igc_vecs, + "igc", + "FAI/IGC Flight Recorder Data Format", + NULL + }, { NULL, NULL, -- 2.30.2